Skip to content

Extract shared composeFilters utility to eliminate duplication between doc-type and page#5431

Open
RohithVangalla1 wants to merge 2 commits into
apostrophecms:mainfrom
RohithVangalla1:refactor/extract-shared-compose-filters-utility
Open

Extract shared composeFilters utility to eliminate duplication between doc-type and page#5431
RohithVangalla1 wants to merge 2 commits into
apostrophecms:mainfrom
RohithVangalla1:refactor/extract-shared-compose-filters-utility

Conversation

@RohithVangalla1

@RohithVangalla1 RohithVangalla1 commented May 22, 2026

Copy link
Copy Markdown
Contributor

The composeFilters() method was duplicated identically between doc-type/index.js and page/index.js, with TODO comments warning developers to 'keep in sync' between the two files. This is fragile — any change to filter composition logic requires updating two files, and forgetting one creates subtle inconsistencies in how filters behave for pages vs. other doc types.

This change extracts the shared logic into lib/compose-filters.js and replaces both implementations with a single call to the shared utility.

The extracted function:

  • Transforms a filters object (keyed by name) into an array
  • Normalizes inputType (defaults to 'select')
  • Adds null choices for non-required filters
  • Sets appropriate nullLabel for dynamic choices
  • Sets default values for checkbox filters

Benefits:

  • Single source of truth for filter composition logic
  • Changes to filter behavior only need to be made in one place
  • Eliminates the risk of the two implementations drifting apart
  • Easier to unit test in isolation

Addresses the TODO comments in both files:
'keep in sync with page/index.js composeFilters' 'keep in sync with doc-type/index.js composeFilters'

Please indicate which branch this PR should merge into:

Check one

  • main

  • latest

  • stable

  • Check if this PR will be resubmitted against another branch

Summary

Summary

Extracts the duplicated composeFilters() method into a shared utility at
lib/compose-filters.js, replacing identical implementations in both
@apostrophecms/doc-type and @apostrophecms/page.

Problem

Both modules had identical 30+ line composeFilters() implementations with
TODO comments warning: "keep in sync with [other file] composeFilters". This is
fragile — any change requires updating two files, and forgetting one creates
subtle inconsistencies in how filters behave for pages vs. other doc types.

Solution

  • New lib/compose-filters.js — pure function that takes a filters object and
    returns the composed array
  • Both modules now call require('../../../../lib/compose-filters')(self.filters)
  • Net reduction of 15 lines of code
  • Fully backward compatible — same behavior, just DRY

Addresses

TODO comments in both files:

"keep in sync with page/index.js composeFilters"
"keep in sync with doc-type/index.js composeFilters"

What are the specific steps to test this change?

  1. Run the website and log in as an admin

  2. Open a piece manager (e.g., Articles) — verify that filters appear correctly in the manager toolbar (dropdowns, checkboxes, radio buttons all render as expected)

  3. Verify that filters with explicit choices show a "None" option when not marked as required

  4. Verify that filters with dynamic choices show the appropriate "Choose one" or "Any" null label

  5. Verify that checkbox-type filters default to an empty array

  6. Navigate to the Pages tree — verify that page-level filters (if configured) also render and behave identically to piece filters

  7. Create a custom piece type with a filter configuration and verify it composes correctly:
    filters: {
    add: {
    myFilter: {
    label: 'My Filter',
    inputType: 'select',
    choices: [
    { value: 'a', label: 'Option A' },
    { value: 'b', label: 'Option B' }
    ]
    }
    }
    }

  8. Confirm the filter shows "None" as an additional choice and functions correctly

What kind of change does this PR introduce?

(Check at least one)

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Build-related changes
  • Other

Make sure the PR fulfills these requirements:

  • It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
  • The changelog is updated
  • Related documentation has been updated
  • Related tests have been updated

If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.

Other information:
This is a pure refactoring with no behavioral change. The extracted lib/compose-filters.js is a pure function with JSDoc documentation that takes a filters object and returns the composed array. Both modules now delegate to this single implementation, ensuring they can never drift apart. Net result is a reduction of 15 lines of code while improving maintainability. Existing tests for both doc-type and page filter behavior should continue to pass without modification since the logic is identical.

The composeFilters() method was duplicated identically between
doc-type/index.js and page/index.js, with TODO comments warning
developers to 'keep in sync' between the two files. This is fragile —
any change to filter composition logic requires updating two files,
and forgetting one creates subtle inconsistencies in how filters
behave for pages vs. other doc types.

This change extracts the shared logic into lib/compose-filters.js and
replaces both implementations with a single call to the shared utility.

The extracted function:
- Transforms a filters object (keyed by name) into an array
- Normalizes inputType (defaults to 'select')
- Adds null choices for non-required filters
- Sets appropriate nullLabel for dynamic choices
- Sets default values for checkbox filters

Benefits:
- Single source of truth for filter composition logic
- Changes to filter behavior only need to be made in one place
- Eliminates the risk of the two implementations drifting apart
- Easier to unit test in isolation

Addresses the TODO comments in both files:
  'keep in sync with page/index.js composeFilters'
  'keep in sync with doc-type/index.js composeFilters'

@boutell boutell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code does not appear to work:

  add missing schema fields
Error: Cannot find module '../../../../lib/compose-filters'
Require stack:
- /Users/boutell/apostrophecms/apostrophe/packages/apostrophe/modules/@apostrophecms/doc-type/index.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
    at Module._load (node:internal/modules/cjs/loader:1043:27)
    at Module.require (node:internal/modules/cjs/loader:1298:19)
    at require (node:internal/modules/helpers:182:18)
    at Object.composeFilters (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/modules/@apostrophecms/doc-type/index.js:1678:24)
    at Object.init (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/modules/@apostrophecms/piece-type/index.js:248:10)
    at self.create (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/lib/moog.js:310:20)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async instantiateModules (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/index.js:677:32)
    at async apostrophe (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/index.js:321:5)
    at async /Users/boutell/apostrophecms/apostrophe/packages/apostrophe/index.js:162:17
    at async module.exports (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/index.js:161:16)
    at async Context.<anonymous> (/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/test/add-missing-schema-fields.js:20:18) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/Users/boutell/apostrophecms/apostrophe/packages/apostrophe/modules/@apostrophecms/doc-type/index.js'
  ]
}

Please review.

The compose-filters.js file is 3 levels up from both:
- modules/@apostrophecms/doc-type/index.js
- modules/@apostrophecms/page/index.js

Path breakdown:
- ../ → up to @apostrophecms/
- ../../ → up to modules/
- ../../../ → up to apostrophe/ (package root)
- ../../../lib/compose-filters → target file

Fixes MODULE_NOT_FOUND error reported in review.
@RohithVangalla1

Copy link
Copy Markdown
Contributor Author

Thanks for catching this @boutell ! You're absolutely right - I had the path wrong.

The issue is that I went up one too many levels. From modules/@apostrophecms/doc-type/index.js to lib/compose-filters.js, it's only 3 levels up (to the apostrophe package root), not 4.

I've fixed it in both files:

  • require('../../../../lib/compose-filters')
  • require('../../../lib/compose-filters')

@boutell

boutell commented Jun 12, 2026

Copy link
Copy Markdown
Member

Thanks. More importantly... have you tested the code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants